/*
  Copyright 2009 Semantic Discovery, Inc. (www.semanticdiscovery.com)

  This file is part of the Semantic Discovery Toolkit.

  The Semantic Discovery Toolkit is free software: you can redistribute it and/or modify
  it under the terms of the GNU Lesser General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  The Semantic Discovery Toolkit is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General Public License
  along with The Semantic Discovery Toolkit.  If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Text;
using System.Collections;
using System.Collections.Generic;

namespace SemanticDiscovery.Util.Range
{
  // todo: implement the Serializable interfaces

  /// <summary>
  /// Class to represent a set of integers.  
  /// </summary>
  /// <author>Spence Koehler</author>
  public class IntegerRange : AbstractNumericRange {

    public int Low {
      get {
        bool didFirst = false;
        int result = 0;
        
        foreach (SimpleRange simpleRange in GetSimpleRanges()) {
          if (!didFirst || simpleRange.LowAsIntRounded < result) {
            result = simpleRange.LowAsIntRounded;
            didFirst = true;
          }
        }
        
        return result;
      }
    }

    public int High {
      get {
        bool didFirst = false;
        int result = 0;
        
        foreach (SimpleRange simpleRange in GetSimpleRanges()) {
          if (!didFirst || simpleRange.HighAsIntRounded > result) {
            result = simpleRange.HighAsIntRounded;
            didFirst = true;
          }
        }
        
        return result;
      }
    }

    public SortedSet<int> Values {
      get {
        SortedSet<int> result = new SortedSet<int>();
        
        foreach (SimpleRange range in GetSimpleRanges()) {
          int? curSize = range.Count;
          if (!curSize.HasValue) {
            result = null;
            break;
          }
          else {
            int low = range.LowAsInt;
            if (!range.IncludesLow) ++low;
            int high = range.HighAsInt;
            if (!range.IncludesHigh) --high;
            for (int i = low; i <= high; ++i) {
              result.Add(i);
            }
          }
        }

        return result;
      }
    }

    /// <summary>
    /// Construct an empty instance.
    /// </summary>
    public IntegerRange() : base() {}

    public IntegerRange(IEnumerable<string> values) : base() {
      ParseValues(values);
    }

    public IntegerRange(string[] values) : base() {
      ParseValues(values);
    }

    public IntegerRange(string value) : base() {
      ParseValue(value);
    }

    public IntegerRange(int number) : base() {
      Add(number);
    }

    public IntegerRange(int left, bool leftInclusive, 
                        int right, bool rightInclusive) : base() 
    {
      Add(left, leftInclusive, right, rightInclusive);
    }

    public IntegerRange(int lowInclusive, int highInclusive) : base() {
      Add(lowInclusive, highInclusive);
    }

    public IntegerRange(int basee, int tolerance, 
                        bool inclusiveStart, bool inclusiveEnd) : base() 
    {
      Add(basee, tolerance, inclusiveStart, inclusiveEnd);
    }

    /// <summary>
    /// Determine whether the other integer range is included in this integer range.
    /// </summary>
    public bool Includes(IntegerRange other) {
      bool result = false;

      SortedSet<int> myValues = this.Values;
      SortedSet<int> otherValues = other.Values;

      if (myValues == null) {
        result = true;
      }
      else if (otherValues != null) {
        if (myValues.Count >= otherValues.Count) {
          foreach(int otherValue in otherValues) {
            if (!myValues.Contains(otherValue)) {
              result = false;
              break;
            }
          }
        }
      }

      return result;
    }

    /// Compare this integer range to the other with return values:
    /// <ul>
    /// <li>-1: If this range's low is less than the other range's low or this range's high is less than the other range's high</li>
    /// <li> 0: If this range is the same as or includes all of the other's values, including if both are infinite</li>
    /// <li> 1: If the other range's low is less than this range's low or the other range's high is less than this range's high</li>
    /// </ul>
    public int CompareTo(IntegerRange other) {
      int result = 0;

      if (this.Low < other.Low && this.High < other.High) {
        result = -1;
      }
      else if (other.Low < this.Low && other.High < this.High) {
        result = 1;
      }

      return result;
    }

    /// Get each of this range's values, unless it is infinite.
    /// @return the discrete values or null.
    public IntegerRange Add(int singleValue) {
      return Add(singleValue, true);
    }

    public IntegerRange Add(int singleValue, bool combineIfContiguous) {
      base.AddRange(new SimpleIntegerRange(singleValue), combineIfContiguous);
      return this;
    }

    public IntegerRange Add(int lowInclusive, int highInclusive) {
      return Add(lowInclusive, highInclusive, true);
    }

    public IntegerRange Add(int lowInclusive, int highInclusive, 
                            bool combineIfContiguous) 
    {
      base.AddRange(new SimpleIntegerRange(lowInclusive, true, highInclusive, true), 
                    combineIfContiguous);
      return this;
    }
    
    public IntegerRange Add(int left, bool leftInclusive, 
                            int right, bool rightInclusive) 
    {
      return Add(left, leftInclusive, right, rightInclusive, true);
    }
    
    public IntegerRange Add(int left, bool leftInclusive, 
                            int right, bool rightInclusive, 
                            bool combineIfContiguous) 
    {
      base.AddRange(new SimpleIntegerRange(left, leftInclusive, right, rightInclusive), 
                    combineIfContiguous);
      return this;
    }

    public IntegerRange Add(int basee, int tolerance, 
                            bool inclusiveStart, bool inclusiveEnd) 
    {
      return Add(basee, tolerance, inclusiveStart, inclusiveEnd, true);
    }

    public IntegerRange Add(int basee, int tolerance, 
                            bool inclusiveStart, bool inclusiveEnd, 
                            bool combineIfContiguous) 
    {
      base.AddRange(new SimpleIntegerRange(basee, tolerance, inclusiveStart, inclusiveEnd), 
                    combineIfContiguous);
      return this;
    }

    protected override SimpleRange buildRange(string number) {
      return new SimpleIntegerRange(number);
    }

    protected override SimpleRange buildRange(string left, bool leftInclusive, 
                                              string right, bool rightInclusive) 
    {
      return new SimpleIntegerRange(left, leftInclusive, right, rightInclusive);
    }

    protected override SimpleRange buildToleranceRange(string basee, string tolerance,
                                                       bool inclusiveStart, bool inclusiveEnd) 
    {
      return new SimpleIntegerRange(basee, tolerance, inclusiveStart, inclusiveEnd);
    }

    private class SimpleIntegerRange : SimpleRange  {
      // todo: implement the Serializable interfaces

      private int low;
      private int high;
      private string str;

      /// Test whether this range includes its low value.
      public bool IncludesLow { get; private set; }
      /// Test whether this range includes its high value.
      public bool IncludesHigh { get; private set; }

      /// This range's low value (as a string).
      public string Low { get { return low.ToString(); } }
      /// The low value as an int.
      public int LowAsInt { get { return low; } }
      public int LowAsIntRounded { get { return low; } }
      /// The low value as a long.
      public long LowAsLong { get { return Convert.ToInt64(low); } }
      public long LowAsLongRounded { get { return Convert.ToInt64(low); } }
      /// The low value as a double.
      public double LowAsDouble { get { return Convert.ToDouble(low); } }

      /// This range's high value (as a string).
      public string High { get { return high.ToString(); } }
      /// The high value as an int.
      public int HighAsInt { get { return high; } }
      public int HighAsIntRounded { get { return high; } }
      /// The high value as an long.
      public long HighAsLong { get { return Convert.ToInt64(high); } }
      public long HighAsLongRounded { get { return Convert.ToInt64(high); } }
      /// The high value as a double.
      public double HighAsDouble { get { return Convert.ToDouble(high); } }

      /// <summary>
      /// Get the number of integers included in this range.
      /// @return null if the range is infinite; otherwise the size.
      /// </summary>
      public int? Count {
        get {
          if (low == int.MinValue || high == int.MaxValue) return null;
          
          int result = high - low - 1;
          if (IncludesLow) ++result;
          if (IncludesHigh) ++result;
          
          return result;
        }
      }

      public SimpleIntegerRange(string number) {
        this.low = "".Equals(number) ? int.MinValue : int.Parse(number);
        this.high = "".Equals(number) ? int.MaxValue : this.low;
        this.IncludesLow = true;
        this.IncludesHigh = true;
        this.str = number;
      }

      public SimpleIntegerRange(string left, bool leftInclusive, 
                         string right, bool rightInclusive) 
      {
        init("".Equals(left) ? int.MinValue : int.Parse(left),
             "".Equals(left) ? true : leftInclusive,
             "".Equals(right) ? int.MaxValue : int.Parse(right),
             "".Equals(right) ? true : rightInclusive);
      }

      public SimpleIntegerRange(string basee, string tolerance, 
                         bool inclusiveStart, bool inclusiveEnd) 
      {
        int baseValue = int.Parse(basee);
        int toleranceValue = "".Equals(tolerance) ? 0 : int.Parse(tolerance);

        init(baseValue - toleranceValue, inclusiveStart, baseValue + toleranceValue, inclusiveEnd);
      }

      public SimpleIntegerRange(int number) {
        init(number, true, number, true);
      }

      public SimpleIntegerRange(int left, bool leftInclusive, 
                         int right, bool rightInclusive) 
      {
        init(left, leftInclusive, right, rightInclusive);
      }

      public SimpleIntegerRange(int basee, int tolerance, 
                         bool inclusiveStart, bool inclusiveEnd) 
      {
        init(basee - tolerance, inclusiveStart, basee + tolerance, inclusiveEnd);
      }

      private void init(int low, bool includeLow, 
                        int high, bool includeHigh) 
      {
        this.low = low;
        this.IncludesLow = includeLow;
        this.high = high;
        this.IncludesHigh = includeHigh;
        this.str = null;  // lazy load

        if (low == high) {
          this.IncludesLow = true;
          this.IncludesHigh = true;
        }
        else if (low > high) {
          // swap left/right
          int lowValue = this.low;
          this.low = this.high;
          this.high = lowValue;

          bool includeLowValue = this.IncludesLow;
          this.IncludesLow = this.IncludesHigh;
          this.IncludesHigh = includeLowValue;
        }
      }

      public int CompareTo(SimpleRange other) {
        int result = low - other.LowAsIntRounded;
        if (result == 0) {
          result = high - other.HighAsInt;
        }
        return result;
      }

      /// Determine whether the integer is in this numeric range.
      public bool Includes(int value) {
        bool result = (value > low && value < high);

        if (!result) {
          if (IncludesLow && value == low) result = true;
          else if (IncludesHigh && value == high) result = true;
        }

        return result;
      }

      /// Determine whether the long is in this numeric range.
      public bool Includes(long value) {
        bool result = (value > low && value < high);

        if (!result) {
          if (IncludesLow && value == low) result = true;
          else if (IncludesHigh && value == high) result = true;
        }

        return result;
      }

      /// Determine whether the double is in this numeric range.
      public bool Includes(double value) {
        bool result = (value > low && value < high);

        if (!result) {
          if (IncludesLow && value == low) result = true;
          else if (IncludesHigh && value == high) result = true;
        }

        return result;
      }

      /// Determine whether the value is in this numeric range.
      public bool Includes(string value) {
        bool result = false;
        try {
          result = Includes(int.Parse(value));
        }
        catch (FormatException e) {
          // non-numeric isn't in range. result is already false.
        }
        return result;
      }

      /// Incorporate the value into this range.
      /// <p>
      /// See AbstractNumericRange.parseValue's javadoc for the format of value.
      public void Include(string value) {
        throw new NotSupportedException("If this is needed, implement it.");
      }

      /// Incorporate the other range's values into this range if this range
      /// combined with the other range is continuous.
      public bool GetUnionIfContinuous(SimpleRange other) {
        bool result = false;

        int otherLowAdj = other.IncludesLow ? 1 : 0;
        int thisLowAdj = this.IncludesLow ? 1 : 0;
        int otherHighAdj = other.IncludesHigh ? 1 : 0;
        int thisHighAdj = this.IncludesHigh ? 1 : 0;

        if (this.Includes(other.LowAsInt - otherLowAdj) || 
            other.Includes(this.HighAsInt + thisHighAdj) ||
            other.Includes(this.LowAsInt - thisLowAdj) || 
            this.Includes(other.HighAsInt + otherHighAdj)) {
          // range extends from min low to max high
          mergeIn(other);
          result = true;
        }

        if (result) str = null;  // need to recompute.

        return result;
      }

      private void mergeIn(SimpleRange other) {
        int otherLowR = other.LowAsIntRounded;
        int otherHighR = other.HighAsIntRounded;
        int otherLowT = other.LowAsInt;
        int otherHighT = other.HighAsInt;

        if (otherLowR < this.low) {
          this.low = otherLowR;
          this.IncludesLow = other.IncludesLow;
        }
        else if (otherLowR == this.low && otherLowT == this.low && other.IncludesLow) {
          this.IncludesLow = true;
        }
        if (otherHighT > this.high) {
          this.high = otherHighT;
          this.IncludesHigh = other.IncludesHigh;
        }
        else if (otherHighT == this.high && otherHighR == this.high && other.IncludesHigh) {
          this.IncludesHigh = true;
        }
      }

      /// Get this range fully represented as a string.
      public string AsString() {
        if (str == null) {
          if (low == high) {
            str = this.Low;
          }
          else {
            StringBuilder result = new StringBuilder();

            bool defaultInclusion = IncludesLow && IncludesHigh;

            // handle unbounded ranges
            if (low == int.MinValue || high == int.MaxValue) {
              if (low == int.MinValue && high == int.MaxValue) {
                result.Append('-');
              }
              else if (low == int.MinValue) {
                result.
                  Append("-(").
                  Append(high).
                  Append(IncludesHigh ? ']' : ')');
              }
              else {  // high == int.MaxValue
                if (!defaultInclusion) result.Append(IncludesLow ? '[' : '(');
                result.
                  Append(low).
                  Append('-');
              }
            }
            else {
              if (!defaultInclusion) result.Append(IncludesLow ? '[' : '(');
              result.
                Append(low).
                Append('-').
                Append(high);
              if (!defaultInclusion) result.Append(IncludesHigh ? ']' : ')');
            }

            str = result.ToString();
          }
        }
        return str;
      }

      public override string ToString() {
        return AsString();
      }

      /// Shift this range's bounds by the given value.
      public void Shift(double value) {
        str = null;  // need to recompute
        this.low += (int)value;
        this.high += (int)value;
      }
    }
  }
}
